use std::sync::Arc;

use glam::DVec3;

use crate::{
    random_standard_uniform, random_unit_vec, reflect, refract, 
    TexSolidColor, HitRecord, Ray, TextureRef,
};

pub trait Material {
    fn scatter(&self, _ray: &Ray, _hit: &HitRecord) -> Option<(DVec3, Ray)> { None }

    fn emitted(&self, _ray: &Ray, _hit: &HitRecord) -> DVec3 { DVec3::ZERO }
}

pub type MaterialRef = Arc<dyn Material>;

pub struct LambertMaterial {
    tex: TextureRef,
}

impl LambertMaterial {
    pub fn new(albedo: DVec3) -> Arc<Self> {
        Arc::new(Self {
            tex: TexSolidColor::new(albedo),
        })
    }

    pub fn from_texture(tex: TextureRef) -> Arc<Self> {
        Arc::new(Self {
            tex,
        })
    }
}

impl Material for LambertMaterial {
    fn scatter(&self, _ray: &Ray, hit: &HitRecord) -> Option<(DVec3, Ray)> {
        let scattered = Ray::new(
            hit.p,
            random_unit_vec(),
        );

        Some((
            self.tex.value(&hit.uv, &hit.p),
            scattered,
        ))
    }
}

pub struct MetalMaterial {
    albedo: DVec3,
    fuzz: f64,
}

impl MetalMaterial {
    pub fn new(albedo: DVec3, fuzz: f64) -> Arc<Self> {
        Arc::new(Self {
            albedo,
            fuzz: fuzz.min(1.0),
        })
    }
}

impl Material for MetalMaterial {
    fn scatter(&self, ray: &Ray, hit: &HitRecord) -> Option<(DVec3, Ray)> {
        let reflected = reflect(&ray.dir, &hit.n) + self.fuzz * random_unit_vec();

        if reflected.dot(hit.n) <= 0.0 {
            return None;
        }

        Some((
            self.albedo,
            Ray::new(hit.p, reflected)
        ))
    }
}

pub struct DielectricMaterial {
    refraction_index: f64,
}

impl DielectricMaterial {
    pub fn new(refraction_index: f64) -> Arc<Self> {
        Arc::new(Self {
            refraction_index,
        })
    }

    fn reflectance(cos_theta: f64, refraction_index: f64) -> f64 {
        // Use Schlick's approximation for reflectance.
        let r0 = (1.0 - refraction_index) / (1.0 + refraction_index);
        let r0 = r0 * r0;
        r0 + (1.0 - r0) * (1.0 - cos_theta).powi(5)
    }
}

impl Material for DielectricMaterial {
    fn scatter(&self, ray: &Ray, hit: &HitRecord) -> Option<(DVec3, Ray)> {
        let attenuation = DVec3::ONE;

        let ri = if hit.front_face {
            1.0 / self.refraction_index
        } else {
            self.refraction_index
        };

        let unit_dir = ray.dir.normalize();
        let cos_theta = (-unit_dir).dot(hit.n).min(1.0);
        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();

        let refracted = if sin_theta * ri > 1.0 || Self::reflectance(cos_theta, ri) > random_standard_uniform() {

            reflect(&unit_dir, &hit.n)
        } else {
            refract(&unit_dir, &hit.n, ri)
        };

        Some((
            attenuation,
            Ray::new(hit.p, refracted),
        ))
    }
}

/// Diffuse light material
/// 
pub struct MatDiffuseLight {
    tex: TextureRef,
}

impl MatDiffuseLight {
    pub fn new(color: DVec3) -> Arc<Self> {
        Arc::new(Self {
            tex: TexSolidColor::new(color),
        })
    }

    #[allow(unused)]
    pub fn from_texture(tex: TextureRef) -> Arc<Self> {
        Arc::new(Self {
            tex,
        })
    }
}

impl Material for MatDiffuseLight {
    fn emitted(&self, _ray: &Ray, hit: &HitRecord) -> DVec3 {
        self.tex.value(&hit.uv, &hit.p)
    }
}

pub struct MatIsotropic {
    tex: TextureRef,
}

impl MatIsotropic {
    pub fn new(color: DVec3) -> Arc<Self> {
        Arc::new(Self {
            tex: TexSolidColor::new(color),
        })
    }
}

impl Material for MatIsotropic {
    fn scatter(&self, _ray: &Ray, hit: &HitRecord) -> Option<(DVec3, Ray)> {
        let albedo = self.tex.value(&hit.uv, &hit.p);

        Some((
            albedo,
            Ray::new(hit.p, random_unit_vec())
        ))
    }
}